iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0

Channel

Channel,即通道,衍生自Charles Antony Richard Hoare的CSP併發模型,在Go語言中具有極其重要的地位。雖然它可用於同步記憶體的訪問,但更適合用於goroutine之間傳遞資訊。就像我們在之前的“Go的並發哲學”章節所提到的那樣,通道在任何規模的程序編碼中都非常有用,因為它足夠靈活,能夠以各種方式組合在一起。

以下是一個channel的基本例子

func main() {
	stringStream := make(chan string)
	go func() {
		stringStream <- "Hello channels!"
	}()
	salutation, ok := <-stringStream // <1>
	fmt.Printf("(%v): %v", ok, salutation)
}
輸出:(true): Hello channels!

stringStream := make(chan string): 這裡創建了一個名為 stringStream 的 channel,專用於傳遞 string 類型的資料。

go func() {...}(): 這裡使用 go 關鍵字啟動了一個新的 goroutine。在這個 goroutine 中,程式向 stringStream channel 發送了一條消息 "Hello channels!"。

salutation, ok := <-stringStream: 這裡,主 goroutine 從 stringStream channel 中接收消息。接收操作會返回兩個值:

第一個值 (salutation) 是從 channel 中接收到的資料。
第二個值 (ok) 是一個布林值,表示接收操作是否成功。如果 channel 已被關閉且沒有其他消息,則這個值會是 false;否則為 true。
fmt.Printf("(%v): %v", ok, salutation): 最後,主 goroutine 會輸出接收到的消息和接收操作的結果 (即 ok 的值)。


接著是關閉channel並且讀取的例子,可以看到這邊的ok會是false,表示channel沒有未讀取的消息。

func main() {
	// 創建一個整數型態的 channel
	intStream := make(chan int)
	
	// 關閉這個 channel
	close(intStream)
	
	// 從已經關閉的 channel 讀取資料。這裡會立即返回兩個值:
	// 第一個值 (integer) 是預設的零值 (對於 int 類型來說是 0),
	// 第二個值 (ok) 是一個布林值,表示 channel 是否仍有未讀取的消息。
	integer, ok := <-intStream // <1>
	
	// 輸出這兩個返回的值
	fmt.Printf("(%v): %v", ok, integer)
}


這個例子是要說即使是已經關閉的channel,還是可以從裡面讀取值。

package main

import (
	"fmt"
)

func main() {
	// 創建一個整數型態的 channel
	ch := make(chan int, 3)

	// 向 channel 發送一些整數
	ch <- 1
	ch <- 2
	ch <- 3

	// 關閉 channel
	close(ch)

	// 從已經關閉的 channel 中讀取資料直到 channel 被耗盡
	for integer := range ch {
		fmt.Println(integer)
	}
}

https://ithelp.ithome.com.tw/upload/images/20230922/20150497YmaLZtIF4n.png

這個圖片很清楚的告訴我們在channel的各種狀態下進行操作會產生的情況:
當一個通道處於 nil 狀態時,任何試圖在該通道上進行的發送或接收都會被阻塞。當一個通道處於開啟狀態時,可以發送和接收信號。當一個通道被置於關閉狀態時,信號不能再被發送,但仍然可以接收信號。

var ch chan int // 在這裡,ch如果沒有被初始化,默認就是 nil

關閉某個通道同樣可以被作為向多個goroutine同時發生消息的方式之一。如果你有多個goroutine在單個 通道上等待,你可以簡單的關閉通道,而不是循環解除每一個goroutine的阻塞。

func main() {
	// 創建一個名為 'begin' 的通道,用於同步 goroutines 的開始時機。
	begin := make(chan interface{})
	var wg sync.WaitGroup
	// 啟動五個 goroutines。
	for i := 0; i < 5; i++ {
		wg.Add(1) // 增加等待組的計數器。
		go func(i int) {
			defer wg.Done() // 完成後,減少等待組的計數器。
			<-begin         // 等待 'begin' 通道被關閉。
			fmt.Printf("%v has begun\n", i)
		}(i)
	}

	// 輸出提示信息。
	fmt.Println("Unblocking goroutines...")
	// 關閉 'begin' 通道,這會解除上面的五個 goroutines 的阻塞。
	close(begin) 
	wg.Wait() // 等待所有的 goroutines 完成。
}

當一個通道被關閉時,從該通道接收的所有操作都會立即完成,而不再阻塞。接收到的值將是通道元素類型的零值。此外,關於已關閉的通道,任何後續的接收操作都可以立即進行,而無需等待。
每個 goroutine 都在 <-begin 這裡阻塞,等待從 begin 通道接收一個值。但當 begin 通道被關閉時,這些正在等待的 goroutines 會立即從 <-begin 這裡返回,因為接收操作立即完成,並且接收到的是零值(在這種情況下是 nil)。


最後是介紹channel的buffer(緩衝),如果通道沒有緩衝,生產者每發送一個整數都會被阻塞,直到主 goroutine 接收到該整數。這會使生產者和消費者更緊密地同步,而緩衝的存在則允許一定程度的非阻塞交互。

但我覺得書上的例子沒有很明顯表達出緩衝與否的情況,所以我用一個比較簡單的對比範例來說明。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 無緩衝的通道
	unbuffered := make(chan string)
	go func() {
		unbuffered <- "unbuffered"
		fmt.Println("Sent to unbuffered channel!")
	}()

	time.Sleep(time.Second * 1) // 故意延遲
	fmt.Println(<-unbuffered) // 取出值後,goroutine才會繼續運行
}

對於無緩衝的通道,goroutine 會在嘗試發送數據到通道時阻塞,直到主函數讀取通道的數據。因此,你會先看到 "Sent to unbuffered channel!" 被打印,然後再是 "unbuffered"。
package main

import (
	"fmt"
	"time"
)

func main() {
	
	// 帶緩衝的通道
	buffered := make(chan string, 1) // 緩衝大小為1
	go func() {
		buffered <- "buffered"
		fmt.Println("Sent to buffered channel!")
	}()

	time.Sleep(time.Second * 1)
	fmt.Println(<-buffered)
}

對於帶有緩衝的通道,由於緩衝的大小為1,goroutine 可以立即將數據發送到通道而不阻塞。所以,你會立即看到 "Sent to buffered channel!",然後是 "buffered"。

上一篇
7.Sync package
下一篇
9.Select, GOMAXPROC
系列文
Concurrency in go 讀書心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言